﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace CIP4.Serializer
{
    public class TokenizerType
    {
        public enum TokenClassEnum
        {
            UnknownTypeToken,
            StartToken,
            NameToken,
            NumberToken,
            SpecialCharacterToken,
            StringToken,
            KeyWordToken,
            EndToken
        }
        private Dictionary<string, bool> _keyWords;
        private List<string> _specialCombinations = new List<string>();
        private List<char> _whiteSpace = new List<char>(" \n\r\t");
        private List<char> _specials = new List<char>("-/.,'(){}[];:&!@#*_=\\<>?|^~`+\"");
        private readonly List<char> _quoteSpecials = new List<char>("'\"");
        private readonly List<char> _numericPreSpecials = new List<char>("-+");
        private readonly List<char> _numericPostSpecials = new List<char>("%");
        private List<char> _numericDigits = new List<char>("1234567890");
        private List<char> _delimiters = new List<char>();
        public string Current { get; set; }
        public TokenClassEnum TokenClass { get; set; }
        public char? PreviousCharacter { get; set; }
        public char? NextCharacter { get; set; }
        public int TokenOffset { get; set; }
        public int TokenStart { get; set; }
        public int TokenCount { get; set; }
        private string _text;
        public string Text
        {
            get
            {
                return _text;
            }
            set
            {
                Current = null;
                TokenClass = TokenClassEnum.StartToken;
                PreviousCharacter = null;
                NextCharacter = null;
                TokenOffset = 0;
                TokenStart = 0;
                TokenCount = 0;
                _lookAhead = null;
                _text = value;
            }
        }
        /// <summary>
        /// TokenStrings will return the tokens in the text as a sequence of strings.
        /// This is a lightweight approach and will consume minimal resources
        /// </summary>
        public IEnumerable<string> TokenStrings
        {
            get
            {
                TokenCount = 0;
                TokenStart = 0;
                TokenOffset = 0;

                // if the text is empty then immediately return EndToken - this will be all the caller sees
                if (string.IsNullOrEmpty(Text))
                {
                    TokenClass = TokenClassEnum.EndToken;
                    yield return null;
                    yield break;
                }

                // Return StartToken to indicate to the caller that we're at the beginning of the text - Current == null
                TokenClass = TokenClassEnum.StartToken;
                yield return Current;

                do
                {
                    if (!GetAToken())
                        break;

                    // Got a token so return it
                    yield return Current;

                    // Set TokenStart which is the fundamental marker for where the current token begins - 
                    // though it will be updated as the code steps over leading white space at the beginning of GetAToken
                    TokenStart = TokenOffset;
                } while (TokenOffset < Text.Length);

                TokenCount += 1;
                PreviousCharacter = Text[Text.Length - 1];
                Current = null;
                NextCharacter = null;
                TokenClass = TokenClassEnum.EndToken;
                // Return EndToken indicating to the caller that we're done
                yield return Current;

                // Now we're really done - this terminates the IEnumerable - it is implied if not supplied but seems nice to have it stated explicitly
                yield break;
            }
        }

        private bool GetAToken()
        {
            for (; TokenOffset < Text.Length && _whiteSpace.Contains(Text[TokenOffset]); TokenOffset++) {}
            TokenStart = TokenOffset;
            if (TokenOffset == Text.Length)
                return false;
            if (_quoteSpecials.Contains(Text[TokenOffset])
                &&
                /* if the quote does not pair then it represents inches or feet */
                    StringPatternCount(Text.Substring(TokenOffset), Text[TokenOffset].ToString()) % 2 == 0 /* pair? */
                )
            {
                TokenClass = TokenClassEnum.StringToken;
                char specialChar = Text[TokenOffset];
                for (TokenOffset += 1; TokenOffset < Text.Length && !(Text[TokenOffset] == specialChar && Text[TokenOffset - 1] != '\\'); TokenOffset++) {}
                TokenOffset += 1;
            }
            else if (ScanNumber(TokenOffset) == TokenClassEnum.NumberToken)
            {
                if (_numericPreSpecials.Contains(Text[TokenOffset]))
                    TokenOffset += 1;
                // The above 'if' guarrantees the first character is not '.' - if a subsequent character is '.', we set decimalPoint to char.MinValue
                // so if another decimal point is encountered in the input, it will result in the termination of the 'for (; TokenOffset... loop'
                char decimalPoint = '.';
                for (; TokenOffset < Text.Length && (_numericDigits.Contains(Text[TokenOffset]) || Text[TokenOffset] == decimalPoint); TokenOffset++)
                {
                    if (Text[TokenOffset] == decimalPoint)
                        decimalPoint = char.MinValue;
                }
                // if a percent sign immediately follows a number, consider it a part of the number
                if (TokenOffset < Text.Length && _numericPostSpecials.Contains(Text[TokenOffset]))
                    TokenOffset += 1;

                TokenClass = TokenClassEnum.NumberToken;
                // if a number is immediately followed by anything other than a delimiter - 
                // the token is a name, not a number - this will also apply to special characters 
                // followed by a non-delimiter character (i.e. not white space or special)
                if (TokenOffset < Text.Length && !_delimiters.Contains(Text[TokenOffset]))
                {
                    for (; TokenOffset < Text.Length && (!_delimiters.Contains(Text[TokenOffset]) || IsASpecialCombination()); TokenOffset++) {}
                    TokenClass = TokenClassEnum.NameToken;
                }
            }
            else if (_specials.Contains(Text[TokenOffset]))
            {
                TokenClass = TokenClassEnum.SpecialCharacterToken;
                if (TokenOffset == Text.Length)
                    TokenOffset = TokenStart;
                TokenOffset += 1;
            }
            else
            {
                for (; TokenOffset < Text.Length && (!_delimiters.Contains(Text[TokenOffset]) || IsASpecialCombination()); TokenOffset++) {}
                TokenClass = TokenClassEnum.NameToken;
            }
            TokenCount += 1;
            if (TokenStart > 0)
                PreviousCharacter = Text[TokenStart - 1];
            if (TokenOffset < Text.Length - 1)
                NextCharacter = Text[TokenOffset];
            else
                NextCharacter = null;
            Current = Text.Substring(TokenStart, TokenOffset - TokenStart);
            if (_keyWords != null && _keyWords.ContainsKey(Current))
                TokenClass = TokenClassEnum.KeyWordToken;
            return true;
        }
        private TokenClassEnum ScanNumber(int tempTokenOffset)
        {
            if (!(_numericDigits.Contains(Text[TokenOffset]) ||
                (_numericPreSpecials.Contains(Text[TokenOffset]) &&
                TokenOffset < Text.Length - 1 && _numericDigits.Contains(Text[TokenOffset + 1]))))
                return TokenClassEnum.UnknownTypeToken;
            if (_numericPreSpecials.Contains(Text[tempTokenOffset]))
                tempTokenOffset += 1;
            // The above 'if' guarrantees the first character is not '.' - if a subsequent character is '.', we set decimalPoint to char.MinValue
            // so if another decimal point is encountered in the input, it will result in the termination of the 'for (; TokenOffset... loop'
            char decimalPoint = '.';
            for (; tempTokenOffset < Text.Length && (_numericDigits.Contains(Text[tempTokenOffset]) || Text[tempTokenOffset] == decimalPoint); tempTokenOffset++)
            {
                if (Text[tempTokenOffset] == decimalPoint)
                    decimalPoint = char.MinValue;
            }
            // if a percent sign immediately follows a number, consider it a part of the number
            if (tempTokenOffset < Text.Length && _numericPostSpecials.Contains(Text[tempTokenOffset]))
                tempTokenOffset += 1;

            TokenClassEnum tempTokenClass = TokenClassEnum.NumberToken;
            // if a number is immediately followed by anything other than a delimiter - 
            // the token is a name, not a number - this will also apply to special characters 
            // followed by a non-delimiter character (i.e. not white space or special)
            if (tempTokenOffset < Text.Length && !_delimiters.Contains(Text[tempTokenOffset]))
            {
                for (; tempTokenOffset < Text.Length && (!_delimiters.Contains(Text[tempTokenOffset]) || IsASpecialCombination()); tempTokenOffset++){}
                tempTokenClass = TokenClassEnum.NameToken;
            }
            return tempTokenClass;
        }
        private bool IsASpecialCombination()
        {
            string remainder = Text.Substring(TokenStart);
            return _specialCombinations.Any(s => remainder.StartsWith(s, StringComparison.CurrentCultureIgnoreCase) && TokenOffset - TokenStart < s.Length);
        }
        /// <summary>
        /// Counts occurrences of a string within another. 
        /// </summary>
        private static int StringPatternCount(string text, string pattern)
        {
            int count = 0;
            int i = 0;
            while ((i = text.IndexOf(pattern, i)) != -1)
            {
                i += pattern.Length;
                count++;
            }
            return count;
        }

        public IEnumerable<TokenType> Tokens
        {
            get
            {
                _lookAhead = new List<TokenType>();
// ToDo: loop relies on the TokenStrings side effect of walking through the tokens - should do this without relying on side-effects
#pragma warning disable 168
                foreach (string tokenString in TokenStrings)
#pragma warning restore 168
                {
                    TokenType token = new TokenType
                    {
                        Text = Current,
                        NextCharacter = NextCharacter,
                        PreviousCharacter = PreviousCharacter,
                        TokenClass = TokenClass,
                        TokenCount = TokenCount,
                        TokenOffset = TokenOffset,
                        TokenStart = TokenStart
                    };
                    if (LookAhead.Count < LookAheadDepth + 1)
                        LookAhead.Add(token);
                    if (LookAhead.Count > LookAheadDepth)
                    {
                        yield return LookAhead[0];
                        LookAhead.RemoveAt(0);
                    }
                }
                while (LookAhead.Count > 0)
                {
                    yield return LookAhead[0];
                    LookAhead.RemoveAt(0);
                }
            }
        }
        private List<TokenType> _lookAhead;
        public List<TokenType> LookAhead
        {
            get
            {
                if (_lookAhead == null)
                    throw new ApplicationException("You must fetch at least the first token (i.e. the token with TokenClass == StartToken) via the Tokens collection before referencing this list");
                return _lookAhead;
            }
        }
        private int LookAheadDepth { get; set; }
        public TokenizerConfigurationType Configuration { get { return new TokenizerConfigurationType(this); } }
        public TokenizerType(string text)
        {
            _delimiters.AddRange(_whiteSpace);
            _delimiters.AddRange(_specials);
            //// removing '-' causes it to be treated as a part of a word where it has non-delimiter character on either side
            //delimiters.Remove('-');
            Text = text;
        }
        public class TokenType
        {
            public TokenClassEnum TokenClass { get; set; }
            public char? PreviousCharacter { get; set; }
            public char? NextCharacter { get; set; }
            public int TokenOffset { get; set; }
            public int TokenStart { get; set; }
            public int TokenCount { get; set; }
            public string Text { get; set; }
            public override string ToString()
            {
                return Text;
            }
        }
        public class TokenizerConfigurationType
        {
            /// <summary>
            /// Allows for special character combinations such as C++ or C# - the list is not 
            /// case sensitive and is expenisve to search so keep it as short as possible
            /// </summary>
            public List<string> SpecialCombinations { get { return new List<string>(Owner._specialCombinations); } }
            /// <summary>
            /// This list is readonly - call Configure to change it
            /// </summary>
            public List<char> WhiteSpace { get { return new List<char>(Owner._whiteSpace); } }
            /// <summary>
            /// This list is readonly - call Configure to change it
            /// </summary>
            public List<char> Specials { get { return new List<char>(Owner._specials); } }
            /// <summary>
            /// This list is readonly - call Configure to change it
            /// </summary>
            public List<char> NumericDigits { get { return new List<char>(Owner._numericDigits); } }
            /// <summary>
            /// This list is readonly - it is built by concatenating WhiteSpace and Specials
            /// </summary>
            public List<char> Delimiters { get { return new List<char>(Owner._delimiters); } }
            public int LookAheadDepth
            {
                get { return Owner.LookAheadDepth; }
                set { Owner.LookAheadDepth = value; }
            }
            private TokenizerType Owner { get; set; }
            internal TokenizerConfigurationType(TokenizerType owner)
            {
                Owner = owner;
            }
            public void Configure(string whiteSpace, string specials, string numericDigits, List<string> specialCombinations)
            {
                Owner._whiteSpace = new List<char>(whiteSpace);
                Owner._specials = new List<char>(specials);
                Owner._numericDigits = new List<char>(numericDigits);
                Owner._delimiters = new List<char>(whiteSpace + specials);
                while (Owner._delimiters.Remove('-')){}
                Owner._specialCombinations = new List<string>(specialCombinations);
            }
            public void Configure(List<string> keyWords, bool caseSensitive)
            {
                Owner._keyWords = caseSensitive ? 
                    new Dictionary<string, bool>() : 
                    new Dictionary<string, bool>(StringComparer.CurrentCultureIgnoreCase);
                foreach (string word in keyWords)
                    Owner._keyWords.Add(word, true);
            }
        }
    }
    public class GoogleTokenizerType : TokenizerType
    {
        public GoogleTokenizerType(string text)
            : base(text)
        {
            // do not put ’  '  ` as delimiters as those would cause something like "don't" to become "don and t"
            Configuration.Configure(
                " /.,(){}[];:&!@#*_=\\<>?|^~\n\r\t",
                "-+%\"",
                "1234567890,.",
                new List<string> { "c#", "c++" }
                );
        }
    }
}
